package ChatSystem;

import java.awt.*;
import java.awt.event.*;
import java.io.File;

import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.*;

/**
 *  The ChatSystem GUI management class.
 */
public class ChatGUI extends JFrame implements LocalUser, WindowListener, ListSelectionListener, ComponentListener {

	private final ChatController _controller;

	private final SpringLayout _chatLayout;
	private final Container _chatContainer;
	private final JLabel _nickname;
	private final JButton _disconnect;

	private final JTable _usersTable;

	private final JLabel _remoteUser;
	private final JTextArea _conversation;

	private final JTextArea _msgToSend;
	private final JButton _sendMessage;
	private final JButton _sendFile;

	private final SpringLayout _loginLayout;
	private final Container _loginContainer;
	private final JTextField _login;
	private final JButton _buttonLogin;

	private boolean _chat;
	private volatile boolean _usersUpdate = false;

	private static final long serialVersionUID = 1L;

	/**
	 * Initializes the GUI in a default state, i.e. presents the login form to the user.
	 * The GUI has 2 main views:
	 *     - the "Login" view consisting of a text field asking the local user's nickname, and a login button.
	 *     - the "Chat" view, composed of the send buttons, conversations views and recipient selection table.
	 * @param controller The ChatController
	 */
	public ChatGUI(ChatController controller) {
		_controller = controller;
		_chat = false;
		this.addComponentListener(this);

		// ==================================================

		_nickname = new JLabel("");
		_disconnect = new JButton("Disconnect");
		_disconnect.addActionListener(new ButtonDisconnect());

		// ==================================================

		_usersTable = new JTable(new AbstractTableModel() {

			private static final long serialVersionUID = 1L;

			public String getColumnName(int col) {
				if (col == 0) {
					return "Nickname";
				}
				else {
					return "IP";
				}
			}

			public int getRowCount() {
				return _controller.getUsers().size();
			}

			public int getColumnCount() {
				return 2;
			}

			public Object getValueAt(int row, int col) {
				String nickname = _controller.getUsers().get(row);
				User user = _controller.getUser(nickname);

				if (col == 0) {
					String value = nickname;
					if(user.getUnreadCount() > 0) {
						value += " (" + new Integer(user.getUnreadCount()).toString() + ")";
					}
					return value;
				}
				else {
					return user.getAddress().toString();
				}
			}

			public boolean isCellEditable(int row, int col) {
				return false;
			}

			public void setValueAt(Object value, int row, int col) {

			}
		});

		_usersTable.getSelectionModel().addListSelectionListener(this);

		// ==================================================

		_remoteUser = new JLabel("Please select an user to talk to.");
		_conversation = new JTextArea(5, 20);
		_conversation.setEditable(false);

		// ==================================================

		_msgToSend = new JTextArea(5, 20);
		_sendMessage = new JButton("Send");
		_sendMessage.addActionListener(new ButtonSend());
		_sendFile = new JButton("Send File");
		_sendFile.addActionListener(new ButtonSendFile());
		_sendMessage.setEnabled(false);
		_sendFile.setEnabled(false);

		// ==================================================

		// ==================================================

		_chatContainer = new Container();
		_chatLayout = new SpringLayout();

		// ==================================================

		_chatContainer.setLayout(_chatLayout);

		JLabel usersLabel = new JLabel("Users :");
		JLabel profileLabel = new JLabel("Profile :");
		_chatContainer.add(profileLabel);
		_chatContainer.add(_nickname);
		_chatContainer.add(_disconnect);
		_chatContainer.add(usersLabel);
		_chatContainer.add(_usersTable);
		_chatContainer.add(_remoteUser);
		_chatContainer.add(_conversation);
		_chatContainer.add(_msgToSend);
		_chatContainer.add(_sendMessage);
		_chatContainer.add(_sendFile);

		_chatLayout.putConstraint(SpringLayout.NORTH, profileLabel, 15, SpringLayout.NORTH, _chatContainer);
		_chatLayout.putConstraint(SpringLayout.NORTH, _nickname, 0, SpringLayout.SOUTH, profileLabel);
		_chatLayout.putConstraint(SpringLayout.NORTH, _disconnect, 0, SpringLayout.SOUTH, _nickname);
		_chatLayout.putConstraint(SpringLayout.NORTH, usersLabel, 15, SpringLayout.SOUTH, _disconnect);
		_chatLayout.putConstraint(SpringLayout.NORTH, _usersTable, 0, SpringLayout.SOUTH, usersLabel);
		_chatLayout.putConstraint(SpringLayout.SOUTH, _chatContainer, 15, SpringLayout.SOUTH, _usersTable);

		_chatLayout.putConstraint(SpringLayout.WEST, profileLabel, 15, SpringLayout.WEST, _chatContainer);
		_chatLayout.putConstraint(SpringLayout.WEST, _nickname, 15, SpringLayout.WEST, _chatContainer);
		_chatLayout.putConstraint(SpringLayout.WEST, _disconnect, 15, SpringLayout.WEST, _chatContainer);
		_chatLayout.putConstraint(SpringLayout.WEST, usersLabel, 15, SpringLayout.WEST, _chatContainer);
		_chatLayout.putConstraint(SpringLayout.WEST, _usersTable, 15, SpringLayout.WEST, _chatContainer);
		_chatLayout.putConstraint(SpringLayout.EAST, _usersTable, 200, SpringLayout.WEST, _usersTable);

		_chatLayout.putConstraint(SpringLayout.NORTH, _remoteUser, 15, SpringLayout.NORTH, _chatContainer);
		_chatLayout.putConstraint(SpringLayout.NORTH, _conversation, 0, SpringLayout.SOUTH, _remoteUser);
		_chatLayout.putConstraint(SpringLayout.NORTH, _sendMessage, 15, SpringLayout.SOUTH, _conversation);
		_chatLayout.putConstraint(SpringLayout.NORTH, _sendFile, 0, SpringLayout.SOUTH, _sendMessage);
		_chatLayout.putConstraint(SpringLayout.SOUTH, _chatContainer, 15, SpringLayout.SOUTH, _sendFile);
		_chatLayout.putConstraint(SpringLayout.NORTH, _msgToSend, 0, SpringLayout.NORTH, _sendMessage);
		_chatLayout.putConstraint(SpringLayout.SOUTH, _msgToSend, 0, SpringLayout.SOUTH, _sendFile);

		_chatLayout.putConstraint(SpringLayout.EAST, _chatContainer, 15, SpringLayout.EAST, _remoteUser);
		_chatLayout.putConstraint(SpringLayout.EAST, _chatContainer, 15, SpringLayout.EAST, _conversation);
		_chatLayout.putConstraint(SpringLayout.EAST, _chatContainer, 15, SpringLayout.EAST, _sendMessage);
		_chatLayout.putConstraint(SpringLayout.EAST, _chatContainer, 15, SpringLayout.EAST, _sendFile);
		_chatLayout.putConstraint(SpringLayout.WEST, _sendMessage, 15, SpringLayout.EAST, _msgToSend);
		_chatLayout.putConstraint(SpringLayout.WEST, _sendFile, 15, SpringLayout.EAST, _msgToSend);

		_chatLayout.putConstraint(SpringLayout.EAST, profileLabel, 0, SpringLayout.EAST, _usersTable);
		_chatLayout.putConstraint(SpringLayout.EAST, _nickname, 0, SpringLayout.EAST, _usersTable);
		_chatLayout.putConstraint(SpringLayout.EAST, _disconnect, 0, SpringLayout.EAST, _usersTable);
		_chatLayout.putConstraint(SpringLayout.EAST, usersLabel, 0, SpringLayout.EAST, _usersTable);

		_chatLayout.putConstraint(SpringLayout.WEST, _remoteUser, 15, SpringLayout.EAST, _usersTable);
		_chatLayout.putConstraint(SpringLayout.WEST, _conversation, 15, SpringLayout.EAST, _usersTable);
		_chatLayout.putConstraint(SpringLayout.EAST, _conversation, -15, SpringLayout.EAST, _chatContainer);
		_chatLayout.putConstraint(SpringLayout.WEST, _msgToSend, 15, SpringLayout.EAST, _usersTable);

		_loginLayout = new SpringLayout();
		_loginContainer = new Container();
		_loginContainer.setLayout(_loginLayout);

		_buttonLogin = new JButton("Login");
		_buttonLogin.addActionListener(new ButtonLogin());
		_login = new JTextField();

		_loginContainer.add(_login);
		_loginContainer.add(_buttonLogin);

		_loginLayout.putConstraint(SpringLayout.NORTH, _login, 15, SpringLayout.NORTH, _loginContainer);
		_loginLayout.putConstraint(SpringLayout.WEST, _login, 15, SpringLayout.WEST, _loginContainer);
		_loginLayout.putConstraint(SpringLayout.EAST, _login, -15, SpringLayout.EAST, _loginContainer);
		_loginLayout.putConstraint(SpringLayout.NORTH, _buttonLogin, 0, SpringLayout.SOUTH, _login);
		_loginLayout.putConstraint(SpringLayout.WEST, _buttonLogin, 15, SpringLayout.WEST, _loginContainer);
		_loginLayout.putConstraint(SpringLayout.EAST, _buttonLogin, -15, SpringLayout.EAST, _loginContainer);
		_loginLayout.putConstraint(SpringLayout.SOUTH, _buttonLogin, -15, SpringLayout.SOUTH, _loginContainer);

		// ==================================================

		this.addWindowListener(this);

		this.displayLogin();

		this.setVisible(true);
	}

	/**
	 * The ActionListener bound to the send message button. Forwards to the ChatController the intent to send the message.
	 */
	private class ButtonSend implements ActionListener {
		@Override
		public void actionPerformed(ActionEvent arg0) {
			String text = _msgToSend.getText();
			if(text.length() > 0) {
				ChatGUI.this.onSendMessage(ChatGUI.this.getSelectedUser(), text);
				ChatGUI.this.updateMessageList();
			}
		}
	}

	/**
	 * Forwards the message to send to the ChatController.
	 * @param user The recipient of the message
	 * @param message The body of the message
	 */
	public void onSendMessage(User user, String message) {
		_controller.sendMessage(user, message);
		_msgToSend.setText("");
	}

	/**
	 * The ActionListener bound to the send message button. Asks the user to choose a file, and upon completion, forwards to the ChatController the intent to send the file.
	 */
	private class ButtonSendFile implements ActionListener {
		@Override
		public void actionPerformed(ActionEvent arg0) {
			JFileChooser fileChooser = new JFileChooser();
			fileChooser.setCurrentDirectory(new File("."));
			fileChooser.setDialogTitle("Choose");
			fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
			fileChooser.setAcceptAllFileFilterUsed(false);
			if(fileChooser.showOpenDialog(ChatGUI.this) == JFileChooser.APPROVE_OPTION) {
				File file = fileChooser.getSelectedFile();
				ChatGUI.this.onSendFile(ChatGUI.this.getSelectedUser(), file);
			}
		}
	}

	/**
	 * Forwards the file to send to the ChatController.
	 * @param user The recipient of the file
	 * @param file The file to send
	 */
	public void onSendFile(User user, File file) {
		_controller.sendFile(user, file);
	}

	/**
	 * The ActionListener bound to the login button. Connects the ChatSystem and the switch to the "Chat" view.
	 */
	private class ButtonLogin implements ActionListener {
		@Override
		public void actionPerformed(ActionEvent arg0) {
			ChatGUI.this.onLogin(_login.getText());
		}
	}

	/**
	 * Performs the login operation.
	 * @param nickname The nickname we will be known to other users by
	 */
	public void onLogin(String nickname) {
		try {
			_nickname.setText(nickname);
			_controller.connect(nickname);
			displayChat();
		}
		catch(Exception e) {
			this.presentError(e.getMessage());
		}
	}

	/**
	 * The ActionListener bound to the logout button. Disconnects the ChatSystem and then switch to the "Login" view.
	 */
	private class ButtonDisconnect implements ActionListener {
		@Override
		public void actionPerformed(ActionEvent arg0) {
			ChatGUI.this.onLogout();
		}
	}
	
	/**
	 * Performs a logout operation and then switch to the "Login" view.
	 */
	public void onLogout() {
		_controller.disconnect();
		displayLogin();
	}

	/**
	 * Switches the current view to "Login".
	 */
	public void displayLogin() {
		_chat = false;
		this.setResizable(false);
		this.setContentPane(_loginContainer);
		
		this.getRootPane().setDefaultButton(_buttonLogin);

		this.setLocationRelativeTo(null);

		this.pack();
		this.setTitle("Login");
		this.revalidate();
		this.repaint();
	}

	/**
	 * Switches the current view to "Chat".
	 */
	public void displayChat() {
		_chat = true;
		this.setContentPane(_chatContainer);
		
		this.getRootPane().setDefaultButton(_sendMessage);

		this.setResizable(true);
		this.setLocationRelativeTo(null);

		this.pack();
		this.setTitle("ChatSystem");
		this.revalidate();
		this.repaint();
	}

	/**
	 * Retrieves the selected user in the GUI when in "Chat" view, symbolized by the selected row in the users table.
	 * @return The currently selected user, or null if none
	 */
	public User getSelectedUser() {
		int selection = _usersTable.getSelectedRow();
		if (selection != -1) {
			return _controller.getUser(selection);
		}

		return null;
	}

	/**
	 * Update the users table's content, when for example a user joins or leaves, or when a unread counter is updated.
	 */
	public void updateUserList() {
		((AbstractTableModel) _usersTable.getModel()).fireTableDataChanged();
	}

	/**
	 * Update the current conversation being displayed, when another user is selected or a message is sent or received.
	 */
	public void updateMessageList() {
		if(_usersUpdate) {
			return;
		}
		_usersUpdate = true;
		
		User user = this.getSelectedUser();

		if(user != null) {
			user.markAsRead();
			int selection = _usersTable.getSelectedRow();
			this.updateUserList();
			_usersTable.addRowSelectionInterval(selection, selection);
			_remoteUser.setText(user.getNickname());
			String conversation = "";
			for (ChatMessage msg : user.getMessages()) {
				conversation += msg.toString() + "\n";
			}
			_conversation.setText(conversation);

			_sendMessage.setEnabled(true);
			_sendFile.setEnabled(true);
		}
		else {
			_remoteUser.setText("Please select an user to talk to.");
			_conversation.setText("");
			_sendMessage.setEnabled(false);
			_sendFile.setEnabled(false);
			this.updateUserList();
		}
		
		_usersUpdate = false;
	}

	/**
	 * Presents a file request to the user, and asks whether the file should be kept or discarded.
	 * @param user The sender of the file.
	 * @param filename The name of the file to be received.
	 */
	public void presentFileRequest(User user, String filename) {
		String[] buttons = {"Accept", "Deny"};

		int result = JOptionPane.showOptionDialog(this, user.getNickname()
				+ " wishes to send you a file named \"" + filename
				+ "\". Do you want to download it?", "File Request",
				JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null,
				buttons, buttons[0]);

		if (result == JOptionPane.YES_OPTION) {
			JFileChooser fileChooser = new JFileChooser() {
				private static final long serialVersionUID = 1L;

				@Override
				public void approveSelection() {
					File f = getSelectedFile();
					if (f.exists()) {
						int result = JOptionPane
								.showConfirmDialog(
										this,
										"A file with the specified name already exists, would you like to overwrite it?",
										"Existing file",
										JOptionPane.OK_CANCEL_OPTION);
						if (result == JOptionPane.OK_OPTION) {
							super.approveSelection();
						}
					} else {
						super.approveSelection();
					}
				}
			};

			fileChooser.setCurrentDirectory(new File("."));
			fileChooser.setDialogTitle("Save");
			fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
			fileChooser.setAcceptAllFileFilterUsed(false);
			fileChooser.setSelectedFile(new File(filename));
			if (fileChooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
				File saveFile = fileChooser.getSelectedFile();
				this.onAcceptFileRequest(user, filename, saveFile);
				return;
			}
		}

		this.onDeclineFileRequest(user, filename);
	}

	/**
	 * Notifies the controller that we wish to accept the file request.
	 * @param user The sender of the file
	 * @param filename The name of the file to be received
	 * @param saveFile The path where we want the file to be saved
	 */
	public void onAcceptFileRequest(User user, String filename, File saveFile) {
		_controller.acceptFileRequest(user, filename, saveFile);
	}

	/**
	 * Notifies the controller that we wish not to accept the file request.
	 * @param user The sender of the file
	 * @param filename The name of the declined file
	 */
	public void onDeclineFileRequest(User user, String filename) {
		_controller.declineFileRequest(user, filename);
	}

	/**
	 * Displays a dialog to the user with an error description.
	 * @param error A description of which error occurred
	 */
	public void presentError(String error) {
		JOptionPane.showConfirmDialog(this,
				"An error occurred during this operation: \"" + error
						+ "\". Please contact the application developper.",
				"Error", JOptionPane.CANCEL_OPTION);
	}

	@Override
	public void windowClosing(WindowEvent e) {
		_controller.exit();
		this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	}

	@Override
	public void componentResized(ComponentEvent e) {
		int width = getWidth();
		int height = getHeight();
		int minWidth = (_chat ? 600 : 300);
		int minHeight = (_chat ? 300 : 105);

		if (width < minWidth) {
			width = minWidth;
		}
		if (height < minHeight) {
			height = minHeight;
		}
		setSize(width, height);
	}
	
	/**
	 * Fired when the user has selected another user from what was selected before.
	 */
	public void onSelectedUserChange() {
		this.updateMessageList();
	}
	
	@Override
	public void valueChanged(ListSelectionEvent arg0) {
		this.onSelectedUserChange();
	}

	/*
	 * Unused interfaces implementations
	 */

	@Override
	public void windowOpened(WindowEvent e) {}

	@Override
	public void windowClosed(WindowEvent e) {}

	@Override
	public void windowIconified(WindowEvent e) {}

	@Override
	public void windowDeiconified(WindowEvent e) {}

	@Override
	public void windowActivated(WindowEvent e) {}

	@Override
	public void windowDeactivated(WindowEvent e) {}

	@Override
	public void componentMoved(ComponentEvent e) {}

	@Override
	public void componentShown(ComponentEvent e) {}

	@Override
	public void componentHidden(ComponentEvent e) {}
}
